﻿#region Using directives.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.Design;
using System.Web.UI.Design.WebControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Threading;
using System.Configuration;

#endregion
using BizElements.BusinessLayer;

namespace BizElements.Web
{
    #region Documentation.
    /// <summary>
    /// <b>SearchProviderRegistry</b> is an application-level storage of <see cref="SearchProvider"/> objects.
    /// </summary>
    /// <remarks><para>SearchProviderRegistry when one application module/form creates a
    /// <see cref="SearchProvider"/> instance and another module uses it to perform search operations.
    /// This is particularly usefull when using a dedicated search page. In that case the search page
    /// may recieve the <b>provider key</b> in the query string and retrieve the prepared SearchProvider 
    /// from the registry. This may significantly reduce round-trips for various search/lookup components. 
    /// </para>
    /// <para>A singleton pattern is used. A single instance of registry is automatically
    /// defined for each ASP.NET aplication.</para>
    /// <para>SearchProviderRegistry periodically checks which providers are no longer used
    /// and removes them from memory. The <see cref="SearchProvider"/> must be re-registered
    /// before it can be used again. Re-registering mechanism automatically detects whether it only
    /// has to prolong the expiration period for the existing provider or whether it has to
    /// add new provider entry to the registry.</para>
    /// </remarks>
    #endregion
    public sealed class SearchProviderRegistry
    {
        private sealed class ProviderWithExpiration
        {
            #region ProviderWithExpiration.

            SearchProvider provider;
            TimeSpan? slidingExpiration;
            DateTime? absoluteExpiration;
            DateTime lastAccessed = Time.Now;

            public ProviderWithExpiration(SearchProvider provider, TimeSpan? slidingExpiration, DateTime? absoluteExpiration)
            {
                this.provider = provider;
                this.slidingExpiration = slidingExpiration;
                this.absoluteExpiration = absoluteExpiration;
            }

            public SearchProvider Provider
            {
                get { return this.provider; }
            }

            public DateTime? AbsoluteExpiration
            {
                get { return this.absoluteExpiration; }
                set { this.absoluteExpiration = value; }
            }

            public TimeSpan? SlidingExpiration
            {
                get { return this.slidingExpiration; }
                set { this.slidingExpiration = value; }
            }

            public DateTime LastAccessed
            {
                get { return this.lastAccessed; }
                set { this.lastAccessed = value; }
            }

            #endregion
        }

        #region FieldPermissions.

        DateTime lastAutoCleanupTime = Time.Now;
        Hashtable registeredProviders = new Hashtable(128);

        #endregion

        #region Singleton. One registry per application.

        private SearchProviderRegistry()
        { }

        const string registryAppKey = "BizElements.Web.SearchProviderRegistry.registryAppKey_e563dec0b9a54546a6d642f4c532cce5";

        /// <summary>
        /// Gets the <b>SearchProviderRegistry</b> for the current ASP.NET application.
        /// </summary>
        /// <returns>An instance of SearchProviderRegistry.</returns>
        public static SearchProviderRegistry GetInstance()
        {
            SearchProviderRegistry registry = HttpContext.Current.Application[registryAppKey] as SearchProviderRegistry;
            if (registry == null)
            {
                registry = new SearchProviderRegistry();
                HttpContext.Current.Application[registryAppKey] = registry;
            }

            return registry;
        }

        #endregion

        #region RegisterProvider.

        /// <summary>
        /// Registers a SearchProvider with the given key.
        /// </summary>
        /// <param name="key">Unique search provider key under which the provider is registered. <b>null</b> is not allowed.</param>
        /// <param name="provider">Provider.</param>
        /// <remarks>If a provider with the specified <b>key</b> is already registered it won't
        /// be overwritten. Instead its expiration period is extended. Thus make sure that you
        /// never register different providers with the same key.</remarks>
        public void RegisterProvider(string key, SearchProvider provider)
        {
            RegisterProvider(key, provider, null, null);
        }

        /// <summary>
        /// Registers a SearchProvider with the given key.
        /// </summary>
        /// <param name="key">Unique search provider key under which the provider is registered. <b>null</b> is not allowed.</param>
        /// <param name="provider">Provider.</param>
        /// <param name="slidingExpiration">The interval between the time the added provider was last accessed and 
        /// the time at which that provider expires. If this value is the equivalent of 20 minutes, the provider 
        /// expires and is removed from the registry 20 minutes after it is last accessed.</param>
        /// <param name="absoluteExpiration">The time at which the added provider expires and is removed from the registry.</param>
        /// <remarks>If a provider with the specified <b>key</b> is already registered it won't
        /// be overwritten. Instead its expiration period is extended. Thus make sure that you
        /// never register different providers with the same key.</remarks>
        public void RegisterProvider(string key, SearchProvider provider, TimeSpan? slidingExpiration, DateTime? absoluteExpiration)
        {
            if (key == null)
                throw new ArgumentNullException("Null is not a valid search provider key.", "key");

            ProviderWithExpiration existingEntry = (ProviderWithExpiration)this.registeredProviders[key];
            if (existingEntry == null)
            {
                this.registeredProviders.Add(key, new ProviderWithExpiration(provider, slidingExpiration, absoluteExpiration));
            }
            else
            {
                // Ensure that new provider is equal to existing provider.
                EnsureEqualProviders(key, existingEntry.Provider, provider);

                existingEntry.LastAccessed = Time.Now;
                if (slidingExpiration != null)
                    existingEntry.SlidingExpiration = slidingExpiration;
                if (absoluteExpiration != null)
                    existingEntry.AbsoluteExpiration = absoluteExpiration;
            }
        }

        private static void EnsureEqualProviders(string key, SearchProvider existingProvider, SearchProvider newProvider)
        {
            // We will only test if engine builders are of the same type and hope for the best.
            string oldEngineBuilder = existingProvider.CreateEngineBuilderInstance().GetType().ToString();
            string newEngineBuilder = newProvider.CreateEngineBuilderInstance().GetType().ToString();
            bool noChanges = (oldEngineBuilder == newEngineBuilder);
            if (!noChanges)
                throw new InvalidOperationException(string.Format("Detected attempt to change engine builder type for existing entry in global registry.\r\nKey: '{0}'.\r\nOld class: '{1}'.\r\nNew class '{2}'\r\nPlease consult documentation for SearchProviderRegistry and SearchProviderBuilder classes to understand usage pattern.", key, oldEngineBuilder, newEngineBuilder));
        }

        #endregion

        #region GetProvider.

        /// <summary>
        /// Gets search provider registered with the given key.
        /// </summary>
        /// <param name="key">Search provider key.</param>
        /// <returns>An instance of <b>SearchProvider</b>; <b>null</b> if no provider is registered with the given key.</returns>
        public SearchProvider GetProvider(string key)
        {
            ProviderWithExpiration entry = (ProviderWithExpiration)this.registeredProviders[key];
            SearchProvider provider = null;
            if (entry != null)
            {
                entry.LastAccessed = Time.Now;
                provider = entry.Provider;
            }

            PerformAutoCleanup();
            return provider;
        }

        #endregion

        #region CleanUp.

        // Indicates whether cleanup operation is in progress. 
        // New cleanup task cannot begin before the current task is finished.
        bool isPerformingCleanup;
        TimeSpan autoCleanupSpan = new TimeSpan(1, 0, 0);
        TimeSpan maxSlidingExpiration = new TimeSpan(4, 0, 0);

        /// <summary>
        /// Gets or sets the value which specifies how often the registry performs auto-cleanup to reduce memory load.
        /// </summary>
        /// <remarks><para>By default this value is set to 1 hour.</para>
        /// <para>SearchProviderRegistry periodically checks which providers are no longer used
        /// and removes them from memory. The <see cref="SearchProvider"/> must be re-registered
        /// before it can be used again. Re-registering mechanism automatically detects whether it only
        /// has to prolong the expiration period for the existing provider or whether it has to
        /// add new provider entry to the registry.</para></remarks>
        public TimeSpan AutoCleanupSpan
        {
            get { return this.autoCleanupSpan; }
            set { this.autoCleanupSpan = value; }
        }

        /// <summary>
        /// Gets or sets the value the maximum allowed sliding expiration.
        /// </summary>
        /// <remarks><para>A <see cref="SearchProvider"/> can be registered with sliding expiration
        /// greater then the one specified in this property, however that setting will be ignored.</para>
        /// <para>By default this value is set to 4 hours.</para>
        /// <para>SearchProviderRegistry periodically checks which providers are no longer used
        /// and removes them from memory. The <see cref="SearchProvider"/> must be re-registered
        /// before it can be used again. Re-registering mechanism automatically detects whether it only
        /// has to prolong the expiration period for the existing provider or whether it has to
        /// add new provider entry to the registry.</para></remarks>
        public TimeSpan MaxSlidingExpiration
        {
            get { return this.maxSlidingExpiration; }
            set { this.maxSlidingExpiration = value; }
        }

        private void PerformAutoCleanup()
        {
            TimeSpan timeSinceLastAutoCleanup = Time.Now - this.lastAutoCleanupTime;
            if (!this.isPerformingCleanup && timeSinceLastAutoCleanup > autoCleanupSpan)
            {
                CleanUp(maxSlidingExpiration);
                this.lastAutoCleanupTime = Time.Now;
            }
        }

        private void CleanUp(TimeSpan? maxSlidingExpiration)
        {
            this.isPerformingCleanup = true;

            StringCollection entryKeysToBeRemoved = new StringCollection();
            DateTime now = Time.Now;
            foreach (DictionaryEntry hashItem in this.registeredProviders)
            {
                ProviderWithExpiration entry = (ProviderWithExpiration)hashItem.Value;
                DateTime expiresOn;
                TimeSpan? slidingExpiration = Min(maxSlidingExpiration, entry.SlidingExpiration);

                // Sliding expiration has higher priority then absolute expiration.
                if (slidingExpiration.HasValue)
                    expiresOn = entry.LastAccessed + slidingExpiration.Value;
                else if (entry.AbsoluteExpiration.HasValue)
                    expiresOn = entry.AbsoluteExpiration.Value;
                else
                    expiresOn = DateTime.MaxValue;

                bool isExpired = (expiresOn < now);
                if (isExpired)
                    entryKeysToBeRemoved.Add((string)hashItem.Key);
            }

            foreach (string key in entryKeysToBeRemoved)
                this.registeredProviders.Remove(key);

            this.isPerformingCleanup = false;
        }

        private static TimeSpan? Min(TimeSpan? lhs, TimeSpan? rhs)
        {
            if (lhs.HasValue)
            {
                return (rhs.HasValue) ? Min(lhs.Value, rhs.Value) : lhs.Value;
            }
            else
            {
                return rhs;
            }
        }

        private static TimeSpan Min(TimeSpan lhs, TimeSpan rhs)
        {
            return (lhs < rhs) ? lhs : rhs;
        }

        #endregion
    }
}